﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;
using Addins.UI.ViewModel;
using EnvDTE;
using EnvDTE80;
using Raona.Code.Contracts.EventArguments;
using Raona.Code.Contracts.Generation;
using Raona.Code.Contracts.Inspection;
using Raona.Code.Inspection;
using WcfClientGenerator.Model.Common;
using WcfClientGenerator.Model.Config;
using WcfClientGenerator.UI.Command;
using WcfClientGenerator.UI.Resx;
using System.IO.Packaging;
using WcfClientGenerator.UI.WcfCGServices;
using System.ServiceModel;

namespace WcfClientGenerator.UI.ViewModel
{
    public class CodeGenerationViewModel : BaseViewModel, IRequestCloseViewModel, IDisposable
    {
        #region Events (Public)

        /// <summary>
        /// Raised when the associated view should be dismissed
        /// </summary>
        public event EventHandler RequestClose;

        #endregion

        #region Constants (Private)

        private const string PackageRelationshipType = @"http://schemas.openxmlformats.org/package/2006/relationships/meta data/core-properties";
        private const string ResourceRelationshipType = @"http://schemas.microsoft.com/opc/2006/sample/required-resource";
        private const string ContractsPackageName = "_package.zip";
        private const string ProxiesPackageName = "_proxies.zip";
        private const string UnpackFolderName = "Content";

        #endregion

        #region Instance Fields (Private)

        /// <summary>
        /// The code generation configuration
        /// </summary>
        private WcfClientConfiguration config = null;
        /// <summary>
        /// The VS root object
        /// </summary>
        private DTE2 applicationObject = null;
        /// <summary>
        /// The message to display during the current code generation step
        /// </summary>
        private string message = null;
        /// <summary>
        /// The number of code generation steps
        /// </summary>
        private int progressStepLimit = -1;
        /// <summary>
        /// The current code generation step
        /// </summary>
        private int currentProgressStep = 0;
        /// <summary>
        /// The command to invoke when the code generation should be interrupted
        /// </summary>
        private DelegatingCommand cancelProcessCommand = null;
        /// <summary>
        /// The command to invoke when the code generation should start
        /// </summary>
        private DelegatingCommand startProcessCommand = null;
        /// <summary>
        /// The object that retrieves the interfaces to use in the code generation process
        /// </summary>
        private IOperationContractInspector contractInspector = null;
        /// <summary>
        /// The current status of the code generation process
        /// </summary>
        private ProcessStatus processStatus = ProcessStatus.Initializing;
        /// <summary>
        /// The text to show in the close button
        /// </summary>
        private string closeText = null;
        /// <summary>
        /// The container that resolves the code generation dependencies
        /// </summary>
        private CompositionContainer container = null;
        /// <summary>
        /// The app domain where the contracts inspector is loaded
        /// </summary>
        private AppDomain contractInspectorAppDomain = null;

        #endregion

        #region Instance Constructors (Public)

        /// <summary>
        /// Initializing constructor
        /// </summary>
        /// <param name="config">The code generation configuration</param>
        /// <param name="applicationObject">The VS root object</param>
        public CodeGenerationViewModel(WcfClientConfiguration config, DTE2 applicationObject)
            : base(SynchronizationContext.Current)
        {
            this.config = config;
            this.applicationObject = applicationObject;
            this.closeText = AppResources.cancel;

            this.InitializeContainer();
        }

        #endregion

        #region Instance Properties (Public)

        /// <summary>
        /// The message to display during the current code generation step
        /// </summary>
        public string Message
        {
            get
            {
                return this.message;
            }
            set
            {
                string oldMessage = this.message;
                this.message = value;
                if (this.message != oldMessage)
                    this.NotifyPropertyChanged("Message");
            }
        }

        /// <summary>
        /// The number of code generation steps
        /// </summary>
        public int ProgressStepLimit
        {
            get
            {
                return this.progressStepLimit;
            }
            set
            {
                int oldProgressStepLimit = this.progressStepLimit;
                this.progressStepLimit = value;
                if (this.progressStepLimit != oldProgressStepLimit)
                    this.NotifyPropertyChanged("ProgressStepLimit");
            }
        }

        /// <summary>
        /// The current code generation step
        /// </summary>
        public int CurrentProgressStep
        {
            get
            {
                return this.currentProgressStep;
            }
            set
            {
                int oldProgressStep = this.currentProgressStep;
                this.currentProgressStep = value;
                if (this.currentProgressStep != oldProgressStep)
                    this.NotifyPropertyChanged("CurrentProgressStep");
            }
        }

        /// <summary>
        /// The command to invoke when the code generation should be interrupted
        /// </summary>
        public ICommand CancelProcessCommand
        {
            get
            {
                if (this.cancelProcessCommand == null)
                {
                    this.cancelProcessCommand = new DelegatingCommand(this.CancelProcess, this.CanCancelProcess);
                }
                return this.cancelProcessCommand;
            }
        }

        /// <summary>
        /// The command to invoke when the code generation should start
        /// </summary>
        public ICommand StartProcessCommand
        {
            get
            {
                if (this.startProcessCommand == null)
                {
                    this.startProcessCommand = new DelegatingCommand(this.StartProcess, this.CanStartProcess);
                }
                return this.startProcessCommand;
            }
        }

        public ProcessStatus ProcessStatus
        {
            get
            {
                return this.processStatus;
            }
            set
            {
                ProcessStatus oldStatus = this.processStatus;
                this.processStatus = value;
                if (this.processStatus != oldStatus)
                    this.NotifyPropertyChanged("ProcessStatus");
            }
        }

        /// <summary>
        /// The message to display during the current code generation step
        /// </summary>
        public string CloseText
        {
            get
            {
                return this.closeText;
            }
            set
            {
                string oldText = this.closeText;
                this.closeText = value;
                if (this.closeText != oldText)
                    this.NotifyPropertyChanged("CloseText");
            }
        }

        [Import]
        public ITemplateGenerator TemplateGenerator { get; set; }

        #endregion

        #region Instance Methods (Public)

        public void Dispose()
        {
            if (this.container != null)
            {
                this.container.Dispose();
                this.container = null;
            }
        }

        #endregion

        #region Instance Methods (Private)

        private void InitializeContainer()
        {
            AssemblyCatalog catalog = new AssemblyCatalog(this.config.TemplatesAssemblyCodeBase);
            this.container = new CompositionContainer(catalog);
            CompositionBatch compositionBatch = new CompositionBatch();
            compositionBatch.AddPart(this);
            this.container.Compose(compositionBatch);
        }


        private bool CanCancelProcess()
        {
            return true;
        }

        private void CancelProcess()
        {
            if (this.processStatus == UI.ProcessStatus.Finished
                || this.processStatus == UI.ProcessStatus.Initializing)
            {
                if (this.RequestClose != null)
                {
                    this.RequestClose(this, EventArgs.Empty);
                }
            }
            else if (this.ProcessStatus == UI.ProcessStatus.Analyzing)
            {
                if (this.contractInspector != null)
                {
                    this.contractInspector.CancelGetInterfaces();
                }
            }
            else if (this.ProcessStatus == UI.ProcessStatus.Generating)
            {
                if (this.TemplateGenerator != null)
                {
                    this.TemplateGenerator.Cancel();
                }
            }
        }

        private bool CanStartProcess()
        {
            return true;
        }

        private void StartProcess()
        {
            this.Message = AppResources.initializingMessage;
            ProjectInspector projectInspector = new ProjectInspector(this.applicationObject);
            Project contractsProject = projectInspector.FindProject(this.config.ContractsProjectName);
            // Get contracts project assembly path
            string outputPath = contractsProject.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value;
            if (!Path.IsPathRooted(outputPath))
                outputPath = Path.Combine(Path.GetDirectoryName(contractsProject.FullName), outputPath);
            string assemblyPath = Path.Combine(outputPath, contractsProject.Properties.Item("OutputFileName").Value);

            if (!this.config.UseGeneratorService)
                this.StartLocalGenerationProcess(assemblyPath);
            else
                this.StartRemoteGenerationProcess(assemblyPath);
        }

        private void StartRemoteGenerationProcess(string assemblyPath)
        {
            string contractsFileName = Path.GetFileName(assemblyPath);
            if (!string.IsNullOrEmpty(this.config.GeneratorServiceUrl)
                && Uri.IsWellFormedUriString(this.config.GeneratorServiceUrl, UriKind.Absolute))
            {
                try
                {
                    this.Message = AppResources.requestingGenerationMessage;
                    this.ProcessStatus = ProcessStatus.Analyzing;

                    string packagePath = System.IO.Path.Combine(this.config.GenerationFolder, CodeGenerationViewModel.ContractsPackageName);
                    this.PackContractAssemblies(Path.GetDirectoryName(assemblyPath), packagePath);
                    if (File.Exists(packagePath))
                    {
                        FileStream packageStream = File.OpenRead(packagePath);
                        WCGServiceClient client = this.GetServiceClient();
                        Task<Stream> responseStreamTask = this.GenerateProxiesAsync(contractsFileName, packageStream, client);
                        responseStreamTask.ContinueWith(t =>
                            {
                                try
                                {
                                    if (packageStream != null)
                                    {
                                        packageStream.Close();
                                        packageStream = null;
                                    }

                                    this.WriteGeneratedFiles(t);
                                }
                                catch (Exception ex)
                                {
                                    Debug.WriteLine(ex.ToString());

                                    this.Message = ex.ToString();
                                    this.ProcessStatus = ProcessStatus.Finished;
                                    this.CloseText = AppResources.close;
                                }
                            });
                    }
                    else
                    {
                        this.Message = AppResources.failedToGenerateContractsPackageMessage;
                        this.ProcessStatus = ProcessStatus.Finished;
                        this.CloseText = AppResources.close;
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);

                    this.Message = ex.Message;
                    this.ProcessStatus = ProcessStatus.Finished;
                    this.CloseText = AppResources.close;
                }
            }
            else
            {
                this.Message = AppResources.invalidGeneratorServiceUrlMessage;
                this.ProcessStatus = ProcessStatus.Finished;
                this.CloseText = AppResources.close;
            }
        }

        private void WriteGeneratedFiles(Task<Stream> task)
        {
            Stream responseStream = task.Result;
            if (responseStream != null)
            {
                this.Message = AppResources.writingFilesMessage;

                using (responseStream)
                {
                    string proxiesPackagePath = System.IO.Path.Combine(this.config.GenerationFolder, CodeGenerationViewModel.ProxiesPackageName);
                    using (FileStream proxiesPackageStream = new FileStream(proxiesPackagePath, FileMode.Create))
                    {
                        responseStream.CopyTo(proxiesPackageStream);
                        proxiesPackageStream.Close();
                    }

                    if (File.Exists(proxiesPackagePath))
                    {
                        string proxiesFolder = Path.Combine(Path.GetDirectoryName(proxiesPackagePath), CodeGenerationViewModel.UnpackFolderName);
                        this.UnpackContractAssemblies(proxiesPackagePath, proxiesFolder);

                        this.synchronizationContext.Send((_) =>
                        {
                            this.AddFilesToProjects(proxiesFolder);

                            this.Message = AppResources.doneMessage;
                            this.ProcessStatus = ProcessStatus.Finished;
                            this.CloseText = AppResources.close;
                        },
                            null
                        );
                    }

                }
            }
        }

        private Task<Stream> GenerateProxiesAsync(string contractsFileName, FileStream packageStream, WCGServiceClient client)
        {
            Task<Stream> responseStreamTask = Task<Stream>.Factory.FromAsync(
                client.BeginGenerateProxies,
                client.EndGenerateProxies,
                new GenerationArguments
                {
                    AsyncContractsFileName = this.config.AsyncContractsFileName,
                    AsyncProxiesFileName = this.config.AsyncProxiesFileName,
                    ClientBaseName = this.config.ClientBaseName,
                    ServiceContractsNamespace = this.config.ServiceContractsNamespace,
                    SpecificUsings = null,
                    SyncContractsFileName = this.config.SyncContractsFileName,
                    SyncProxiesFileName = this.config.SyncProxiesFileName,
                    ContractsAssemblyFileName = contractsFileName,
                    AsyncProxiesNeeded = this.config.AsyncDestinationProjects.Count > 0,
                    SyncProxiesNeeded = this.config.SyncDestinationProjects.Count > 0
                },
                packageStream,
                null);
            return responseStreamTask;
        }

        private void AddFilesToProjects(string destinationFolder)
        {
            IEnumerable<string> generatedFiles = Directory.EnumerateFiles(destinationFolder);

            this.CloseOpenFiles(generatedFiles.Select(path => Path.GetFileName(path)).ToList());

            foreach (string filePath in generatedFiles)
            {
                if (this.IsAsyncProjectFile(filePath))
                    this.AddFileToAsyncProjects(filePath);
                else if (this.IsSyncProjectFile(filePath))
                    this.AddFileToSyncProjects(filePath);
            }
        }

        private void AddFileToSyncProjects(string filePath)
        {
            if (this.config.SyncDestinationProjects.Count > 0)
                this.AddFilesToProjects(this.config.SyncDestinationProjects, filePath);
        }

        private void AddFilesToProjects(List<string> projectsList, string filePath)
        {
            ProjectInspector projectInspector = new ProjectInspector(this.applicationObject);
            foreach (string projectName in projectsList)
            {
                Project project = projectInspector.FindProject(projectName);
                if (project != null)
                    projectInspector.AddFileToProject(project, filePath);
            }
        }

        private bool IsSyncProjectFile(string filePath)
        {
            string fileName = Path.GetFileName(filePath);

            return fileName == this.config.SyncContractsFileName
                || fileName == this.config.SyncProxiesFileName;
        }

        private void AddFileToAsyncProjects(string filePath)
        {
            if (this.config.AsyncDestinationProjects.Count > 0)
                this.AddFilesToProjects(this.config.AsyncDestinationProjects, filePath);
        }

        private bool IsAsyncProjectFile(string filePath)
        {
            string fileName = Path.GetFileName(filePath);

            return fileName == this.config.AsyncContractsFileName
                || fileName == this.config.AsyncProxiesFileName;
        }

        private WCGServiceClient GetServiceClient()
        {
            NetTcpBinding binding = new NetTcpBinding();
            binding.MaxReceivedMessageSize = WcfClientGeneratorConfig.Instance.GeneratorServiceConfiguration.MaxReceivedMessageSize;
            binding.TransferMode = TransferMode.Streamed;
            EndpointAddress address = new EndpointAddress(this.config.GeneratorServiceUrl);

            WCGServiceClient client = new WCGServiceClient(binding, address);

            return client;
        }

        private void StartLocalGenerationProcess(string assemblyPath)
        {
            if (this.contractInspector == null)
            {
                // Create contracts inspector
                this.contractInspector = this.CreateInspector(assemblyPath, "Raona.Code.AssemblyInspection.AssemblyOperationContractInspector2", "Raona.Code.AssemblyInspection.dll");

                this.contractInspector.ProgressChanged += new EventHandler<ProgressEventArgs>(this.OnContractInspectorProgressChanged);
                this.contractInspector.ProcessFinished += new EventHandler<ProgressResultEventArgs>(this.OnContractInspectorProcessFinished);
            }
            this.Message = AppResources.processingMessage;
            this.ProcessStatus = ProcessStatus.Analyzing;
            try
            {
                this.contractInspector.GetInterfacesAsync(config.ClientBaseName, config.ServiceContractsNamespace);
            }
            catch (AggregateException)
            {
                this.Message = AppResources.cancelledMessage;
                this.ProcessStatus = ProcessStatus.Finished;
            }
            catch (OperationCanceledException operationCanceledException)
            {
                this.Message = operationCanceledException.Message;
                this.ProcessStatus = ProcessStatus.Finished;
            }
        }

        private IOperationContractInspector CreateInspector(string contractsAssemblyPath, string inspectorTypeName, string inspectorAssemblyFileName)
        {
            string location = this.ContractsInspectorAppDomainCodeBase;
            string inspectorAssemblyPath = Path.Combine(location, inspectorAssemblyFileName);
            AppDomainSetup setup = new AppDomainSetup
            {
                ApplicationBase = location
            };
            // Create a new app domain
            this.contractInspectorAppDomain = AppDomain.CreateDomain("CodeGenerationDomain", null, setup);
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(this.OnAssemblyResolve);
            AssemblyName assemblyName = AssemblyName.GetAssemblyName(inspectorAssemblyPath);
            // Create the contracts inspector in the new domain so it can be unloaded and
            // therefore the contracts assembly is not locked by the addin
            IOperationContractInspector inspector = (IOperationContractInspector)this.contractInspectorAppDomain
                .CreateInstanceFromAndUnwrap(inspectorAssemblyPath, inspectorTypeName, false, BindingFlags.Default, null, new object[] { contractsAssemblyPath }, null, null);

            return inspector;
        }

        private Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
        {
            string codeBase = this.ContractsInspectorAppDomainCodeBase;
            AssemblyName assemblyName = new AssemblyName(args.Name);
            string modulePath = string.Format("{0}\\{1}.dll", codeBase, assemblyName.Name);
            if (File.Exists(modulePath))
                assemblyName = AssemblyName.GetAssemblyName(modulePath);
            return ((AppDomain)sender).Load(assemblyName);
        }

        private string ContractsInspectorAppDomainCodeBase
        {
            get
            {
                string location = Path.GetDirectoryName(this.GetType().Assembly.Location);
                return location;
            }
        }

        private void UnloadInspector()
        {
            this.UnloadInspector(null, null);
        }

        private void UnloadInspector(IEnumerable<Raona.Code.Model.Namespace> namespaces, Exception exception)
        {
            if (this.contractInspector != null)
            {
                this.contractInspector.ProgressChanged -= new EventHandler<ProgressEventArgs>(this.OnContractInspectorProgressChanged);
                this.contractInspector.ProcessFinished -= new EventHandler<ProgressResultEventArgs>(this.OnContractInspectorProcessFinished);
                this.contractInspector.Dispose();
                this.contractInspector = null;
            }
            try
            {
                Task task = new Task(this.UnloadInspectorAppDomain);
                System.Threading.Timer timer = new System.Threading.Timer(_ =>
                    task.Start(),
                    null,
                    100,
                    System.Threading.Timeout.Infinite);
                if (exception == null)
                {
                    task.ContinueWith(_ =>
                        {
                            timer.Dispose();
                            if (namespaces != null)
                                this.CreateProxies(namespaces);
                        });
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }

        }

        private void UnloadInspectorAppDomain()
        {
            AppDomain.CurrentDomain.AssemblyResolve -= new ResolveEventHandler(this.OnAssemblyResolve);
            if (this.contractInspectorAppDomain != null)
            {
                AppDomain.Unload(this.contractInspectorAppDomain);
                this.contractInspectorAppDomain = null;
            }
        }

        private void CreateProxies(IEnumerable<Raona.Code.Model.Namespace> namespaces)
        {
            this.ProcessStatus = UI.ProcessStatus.Generating;

            string configuration = SerializationManager.XmlSerializeString(this.config);
            ITemplateGenerator generator = this.TemplateGenerator;
            generator.Configuration = configuration;
            generator.Namespaces = namespaces;
            this.ProgressStepLimit = generator.StepCount;
            generator.ProgressChanged += (sender, e) =>
                {
                    this.Message = e.Message;
                    this.CurrentProgressStep += 1;
                };
            generator.ProcessFinished += (sender, e) =>
                {
                    if (e.Error != null)
                    {
                        System.Diagnostics.Debug.WriteLine(e.Error.Message);
                        this.Message = e.Error.Message;
                        this.ProcessStatus = UI.ProcessStatus.Finished;
                        this.CloseText = AppResources.close;
                    }
                    else if (e.GeneratedFiles != null)
                    {
#if (DEBUG)
                        foreach (string project in e.GeneratedFiles.Keys)
                        {
                            System.Diagnostics.Debug.WriteLine(project);
                            foreach (GeneratedFile generatedFile in e.GeneratedFiles[project])
                            {
                                System.Diagnostics.Debug.WriteLine("  File: {0}, Size: {1}", generatedFile.FileName, generatedFile.FullPath != null ? generatedFile.FullPath.Length : 0);
                            }
                        }
#endif
                        this.Message = AppResources.writingFilesMessage;

                        // Delete previous versions of the files
                        List<string> fileNames = e.GeneratedFiles.Values.SelectMany(gf => gf.Select(f => f.FileName)).Distinct().ToList();
                        this.CloseOpenFiles(fileNames);

                        // Include generated files in their corresponding projects

                        this.synchronizationContext.Send((_) =>
                            {
                                ProjectInspector projectInspector = new ProjectInspector(this.applicationObject);
                                foreach (string projectName in e.GeneratedFiles.Keys)
                                {
                                    Project project = projectInspector.FindProject(projectName);
                                    if (project != null)
                                    {
                                        foreach (GeneratedFile generatedFile in e.GeneratedFiles[projectName])
                                        {
                                            string path = Path.Combine(this.config.GenerationFolder, generatedFile.FullPath);
                                            projectInspector.AddFileToProject(project, path);
                                        }
                                    }
                                }
                            }, null);

                        this.Message = AppResources.doneMessage;
                        this.CloseText = AppResources.close;
                    }
                    this.ProcessStatus = UI.ProcessStatus.Finished;
                };
            generator.GenerateAsync();
        }

        private void CloseOpenFiles(List<string> fileNames)
        {
            fileNames.ForEach(fileName =>
            {

                // Close any previous versions of the generated files that may be open
                Document document = null;
                try
                {
                    document = this.applicationObject.Documents.Item(fileName);
                }
                catch (ArgumentException)
                {
                }
                if (document != null)
                    document.Close(vsSaveChanges.vsSaveChangesNo);
            });
        }

        private void PackContractAssemblies(string folderPath, string destinationPath)
        {
            using (Package package = Package.Open(destinationPath, FileMode.Create))
            {
                DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);
                foreach (FileInfo fileInfo in directoryInfo.EnumerateFiles("*.dll"))
                {
                    string fileName = ".\\" + Path.GetFileName(fileInfo.Name);
                    Uri uri = PackUriHelper.CreatePartUri(new Uri(fileName, UriKind.Relative));
                    PackagePart part = package.CreatePart(uri, "", CompressionOption.Normal);
                    using (FileStream fileStream = fileInfo.OpenRead())
                    {
                        using (Stream partStream = part.GetStream())
                        {
                            fileStream.CopyTo(partStream);
                        }
                    }
                }
            }
        }

        private void UnpackContractAssemblies(string packagePath, string destinationFolder)
        {
            DirectoryInfo directoryInfo = new DirectoryInfo(destinationFolder);
            if (directoryInfo.Exists)
                directoryInfo.Delete(true);
            directoryInfo.Create();

            using (Package package = Package.Open(packagePath, FileMode.Open, FileAccess.Read))
            {
                foreach (PackagePart part in package.GetParts())
                {
                    string partFileName = part.Uri.ToString().Trim('/');
                    string filePath = Path.Combine(destinationFolder, partFileName);
                    using (Stream partStream = part.GetStream(FileMode.Open, FileAccess.Read))
                    {
                        using (Stream fileStream = File.Open(filePath, FileMode.Create, FileAccess.Write))
                        {
                            partStream.CopyTo(fileStream);
                        }
                    }
                }
            }
        }

        #endregion

        #region Event Handlers (Private)

        private void OnContractInspectorProcessFinished(object sender, ProgressResultEventArgs e)
        {
            if (e.Error != null)
            {
                this.UnloadInspector(null, e.Error);
                System.Diagnostics.Debug.WriteLine(e.Error.Message);
                this.Message = e.Error.Message;
                this.ProcessStatus = UI.ProcessStatus.Finished;
                this.CloseText = AppResources.close;
            }
            else
            {
                this.UnloadInspector(e.Namespaces.Values.AsEnumerable(), null);
            }
        }

        private void OnContractInspectorProgressChanged(object sender, ProgressEventArgs e)
        {
            Dispatcher.CurrentDispatcher.Invoke(new Action(() =>
                {
                    this.Message = e.Message;
                }),
                null);
        }

        #endregion
    }
}
